/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.velocityme.session;

import com.velocityme.entity.Activity;
import com.velocityme.entity.ChangeDelta;
import com.velocityme.entity.ChangeDeltaItem;
import com.velocityme.entity.Contactable;
import com.velocityme.entity.State;
import com.velocityme.entity.Status;
import com.velocityme.entity.TaskStateMachine;
import com.velocityme.entity.Time;
import com.velocityme.entity.Transition;
import com.velocityme.enums.ChangeDeltaItemType;
import com.velocityme.enums.ChangeDeltaType;
import com.velocityme.enums.Permission;
import java.io.File;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TemporalType;

/**
 *
 * @author rcrida
 */
@Stateless
public class ActivityBean implements ActivityLocal {

    @EJB
    private SearchLocal searchBean;
    @EJB
    private TaskLocal taskBean;
    @PersistenceContext
    private EntityManager em;
    State startState;

    private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

    @PostConstruct
    protected void initialize() {
        startState = (State) em.createNamedQuery("findStartState").getSingleResult();
    }

    public void logCreate(KeyLocal key, Activity activity, ChangeDelta changeDelta, boolean interested) {
        taskBean.logCreate(activity, changeDelta, interested);
        Date tNow = new Date();
        activity.setStartDate(tNow);
        long duration = (long) (activity.getExpectedDuration_min() * 60000);
        activity.setStopDate(new Date(tNow.getTime() + duration));
        activity.setRemainingDuration_min(activity.getExpectedDuration_min());
        changeDelta.addItem(ChangeDeltaItemType.PERCENTAGE_COMPLETE, Byte.toString(activity.getPercentageComplete()));
        changeDelta.addItem(ChangeDeltaItemType.EXPECTED_DURATION, activity.expectedDurationToString());
        changeDelta.addItem(ChangeDeltaItemType.TASKSTATEMACHINE, activity.getTaskStateMachine().toString());
        performStartTransition(key, activity, changeDelta);
    }

    public void logEdit(KeyLocal key, Activity activity, Activity current, ChangeDelta changeDelta) {
        taskBean.logEdit(activity, current, changeDelta);
        // copy settings for creator, start and stop dates
        activity.setStartDate(current.getStartDate());
        activity.setStopDate(current.getStopDate());
        activity.setState(current.getState());
        activity.setStatus(current.getStatus());
        if (activity.getPercentageComplete() != current.getPercentageComplete()) {
            changeDelta.addItem(ChangeDeltaItemType.PERCENTAGE_COMPLETE, Byte.toString(activity.getPercentageComplete()));
        }
        if (activity.getExpectedDuration_min() != current.getExpectedDuration_min()) {
            changeDelta.addItem(ChangeDeltaItemType.EXPECTED_DURATION, activity.expectedDurationToString());
        }
        if (!activity.getTaskStateMachine().equals(current.getTaskStateMachine())) {
            changeDelta.addItem(ChangeDeltaItemType.TASKSTATEMACHINE, activity.getTaskStateMachine().toString());
            performStartTransition(key, activity, changeDelta);
        } else {
            // TODO: send notification
        }
    }

    private void performStartTransition(KeyLocal key, Activity activity, ChangeDelta changeDelta) {
        TaskStateMachine taskStateMachine = activity.getTaskStateMachine();
        Transition startTransition = (Transition) em.createNamedQuery("findTransitionBySourceStateAndTaskStateMachine").
                setParameter("sourceState", startState).
                setParameter("taskStateMachine", taskStateMachine).
                getSingleResult();
        performTransition(key, activity, startTransition, changeDelta);
    }

    private void performTransition(KeyLocal key, Activity activity, Transition transition, ChangeDelta changeDelta) {
        activity.setState(transition.getDestinationState());
        activity.setStatus(transition.getStatus());
        Set<Contactable> previousResponsibles = new HashSet<Contactable>(activity.getResponsibleContactables());
//        activity.setResponsibleContactables(searchBean.getContactablesAssignedToTransition(key, activity, transition));
//        activity.setInterestedContactables(searchBean.getContactablesAssignedToTaskStateMachine(key, activity, taskStateMachine));
        changeDelta.addItem(ChangeDeltaItemType.STATE, activity.getState().toString());
        changeDelta.addItem(ChangeDeltaItemType.STATUS, activity.getStatus().toString());
        for (Contactable contactable : activity.getResponsibleContactables()) {
            changeDelta.addItem(ChangeDeltaItemType.ADD_RESPONSIBILITY, contactable.toString());
        }
        Collection<Contactable> newResponsibleContactables = new ArrayList<Contactable>(activity.getResponsibleContactables());
        newResponsibleContactables.removeAll(previousResponsibles);
//        NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfResponsibility(key, activity, newResponsibleContactables);
//        NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(key, activity, interestedContactablesLocal);
    }

    public int getRemainingDuration_min(Activity activity, Date stop) {
        int remaining_min = activity.getExpectedDuration_min();
        Collection<Time> times = em.createNamedQuery("findActivityTimeBeforeDate").
                setParameter("activity", activity).
                setParameter("date", stop, TemporalType.DATE).
                getResultList();
        Date date = null;
        for (Time time : times) {
            if (date == null || date.before(time.getStopTime())) {
                date = time.getStopTime();
                remaining_min = time.getRemainingDuration_min();
            }
        }
        return remaining_min;
    }

    public List<Time> getTimeAfterDate(KeyLocal key, Activity activity, Date date) {
        key.validateKey();
        return em.createNamedQuery("findActivityTimeAfterDate").
                setParameter("activity", activity).
                setParameter("date", date).
                getResultList();
    }

    public void changeState(KeyLocal key, long activityId, long transitionId, String message, byte percentageComplete, File file, String contentType, InputStream stream) {
        Activity activity = em.find(Activity.class, activityId);
        key.assertHasPermission(activity, Permission.TASK_CHANGE_STATE);
        Transition transition = em.find(Transition.class, transitionId);
        Status status = transition.getStatus();
        State destinationState = transition.getDestinationState();
        activity.setStatus(status);
        activity.setState(destinationState);
        Set<Contactable> previousResponsibles = new HashSet<Contactable>(activity.getResponsibleContactables());
        activity.setResponsibleContactables(searchBean.getContactablesAssignedToTransition(key, activity, transition));
        ChangeDelta changeDelta = activity.addChangeDelta(ChangeDeltaType.CHANGE_STATE, key.getUser());
        changeDelta.addItem(ChangeDeltaItemType.STATUS, status.toString());
        changeDelta.addItem(ChangeDeltaItemType.STATE, destinationState.toString());
        changeDelta.addItem(ChangeDeltaItemType.MESSAGE, message);
        if (destinationState.equals(startState)) {
            activity.setPercentageComplete((byte) 100);
            activity.setRemainingDuration_min(0);
            changeDelta.addItem(ChangeDeltaItemType.PERCENTAGE_COMPLETE, "100 (auto)");
            changeDelta.addItem(ChangeDeltaItemType.EXPECTED_DURATION, "0 (auto)");
            // create an empty time entry to set remaining to 0
            Time time = new Time();
            time.setDescription("Auto complete");
            time.setState(startState);
            Date tNow = new Date();
            time.setStartTime(tNow);
            time.setStopTime(tNow);
            time.setRemainingDuration_min(0);
            time.setPerson(key.getPerson());
            time.setActivity(activity);
            em.persist(time);
            // the task has completed so update the cached dates
            updateCachedDates(activity, changeDelta.getCreationTime().getTime(), changeDelta.getCreationTime().getTime());
        } else {
            if (percentageComplete != activity.getPercentageComplete()) {
                activity.setPercentageComplete(percentageComplete);
                changeDelta.addItem(ChangeDeltaItemType.PERCENTAGE_COMPLETE, Byte.toString(percentageComplete));
            }
        }
//        FileAttachmentSessionUtil.getLocalHome().create().createFileAttachment(changeDelta, p_file, p_contentType, p_stream);
        for (Contactable contactable : activity.getResponsibleContactables()) {
            changeDelta.addItem(ChangeDeltaItemType.ADD_RESPONSIBILITY, contactable.toString());
        }
        em.persist(changeDelta);
        Set<Contactable> newResponsibleContactables = new HashSet(activity.getResponsibleContactables());
        newResponsibleContactables.removeAll(previousResponsibles);
//        NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfResponsibility(p_keyLocal, activity, newResponsibleContactablesLocal);
//        NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(p_keyLocal, activity, new HashSet(activity.getInterestedContactablesLocal()));
    }

    public Activity findActivity(KeyLocal key, Long activityId) {
        Activity activity = em.find(Activity.class, activityId);
        key.assertHasPermission(activity, Permission.NODE_VIEW);
        return activity;
    }

    private class StartStopPair {

        private long m_start;
        private long m_stop;

        public StartStopPair(long p_start, long p_stop) {
            m_start = p_start;
            m_stop = p_stop;
        }

        public long getStart() {
            return m_start;
        }

        public long getStop() {
            return m_stop;
        }

        public void include(StartStopPair oth) {
            include(oth.m_start, oth.m_stop);
        }

        public void include(long p_start, long p_stop) {
            if (m_start > p_start) {
                m_start = p_start;
            }
            if (m_stop < p_stop) {
                m_stop = p_stop;
            }
        }
    }

    /**
     * Get the cached start and stop dates for a task. If they haven't already
     * been calculated then do it now!
     **/
    private StartStopPair getCachedDates(Activity activity) {
        if (activity.getCachedStartDate() == null || activity.getCachedStopDate() == null) {
            calculateCachedDates(activity);
        }
        long start = activity.getCachedStartDate() != null ? activity.getCachedStartDate().getTime() : Long.MAX_VALUE;
        long stop = activity.getCachedStopDate() != null ? activity.getCachedStopDate().getTime() : Long.MIN_VALUE;
        return new StartStopPair(start, stop);
    }

    /**
     * Update the existing cached start and stop dates for a task by including
     * a new interval.
     **/
    private void updateCachedDates(Activity activity, long start, long stop) {
        StartStopPair taskInterval = getCachedDates(activity);
        taskInterval.include(start, stop);
        activity.setCachedStartDate(new Date(taskInterval.getStart()));
        activity.setCachedStopDate(new Date(taskInterval.getStop()));
    }

    /**
     * Calculate the correct cached start and stop dates for a task based on
     * assigned times and state changed to start state if appropriate
     **/
    private void calculateCachedDates(Activity activity) {
        StartStopPair startStopPair = new StartStopPair(Long.MAX_VALUE, Long.MIN_VALUE);
        // iterate over allocated times to find earliest start and latest stop
        for (Time time : activity.getTimes()) {
            startStopPair.include(time.getStartTime().getTime(), time.getStopTime().getTime());
        }
        // now check the state changes
        // only consider changes to Start State
        for (ChangeDelta cdLocal : (List<ChangeDelta>) em.createNamedQuery("findChangeDeltaByNode").setParameter("node", activity).getResultList()) {
            if (cdLocal.getChangeDeltaType() == ChangeDeltaType.CHANGE_STATE) {
                for (ChangeDeltaItem cdiLocal : cdLocal.getChangeDeltaItems()) {
                    if (cdiLocal.getChangeDeltaItemType() == ChangeDeltaItemType.STATE
                            && cdiLocal.getDescription().equals("Start State")) {
                        startStopPair.include(cdLocal.getCreationTime().getTime(), cdLocal.getCreationTime().getTime());
                    }
                }
            }
        }
        if (startStopPair.getStart() != Long.MAX_VALUE) {
            activity.setCachedStartDate(new Date(startStopPair.getStart()));
        } else {
            activity.setCachedStartDate(null);
        }
        if (startStopPair.getStop() != Long.MIN_VALUE) {
            activity.setCachedStopDate(new Date(startStopPair.getStop()));
        } else {
            activity.setCachedStopDate(null);
        }
    }

    public void createTime(KeyLocal key, Time time, byte percentageComplete) {
        key.assertHasPermission(time.getActivity(), Permission.TIME_CREATE);
//        if (!time.getStartTime().before(time.getStopTime()))
//            throw new InvalidTimeException();
        percentageComplete = calculateRemaining(key, time.getActivity(), percentageComplete, time);
        // update the cached times by adding the new interval
        updateCachedDates(time.getActivity(), time.getStartTime().getTime(), time.getStopTime().getTime());
        updateRemaining(key, time.getActivity(), percentageComplete);
        em.persist(time);
    }

    public void editTime(KeyLocal key, Time time, byte percentageComplete) {
        Activity activity = time.getActivity();
        key.assertHasPermission(activity, Permission.TIME_EDIT);
//        if (time.getStartTime().before(time.getStopTime())) {
            percentageComplete = calculateRemaining(key, activity, percentageComplete, time);
            ChangeDelta changeDelta = activity.addChangeDelta(ChangeDeltaType.EDIT_TIME, key.getUser());
            Time current = em.find(Time.class, time.getId());
            changeDelta.addItem(ChangeDeltaItemType.PERSON, time.getPerson().toString());
            if (!current.getDescription().equals(time.getDescription())) {
                changeDelta.addItem(ChangeDeltaItemType.DESCRIPTION, current.getDescription() + " -> " + time.getDescription());
            }
            if (!current.getState().getId().equals(time.getState().getId())) {
                changeDelta.addItem(ChangeDeltaItemType.STATE, current.getState().toString() + " -> " + time.getState().toString());
            }
            if (!current.getStartTime().equals(time.getStartTime())) {
                changeDelta.addItem(ChangeDeltaItemType.START_TIME, formatter.format(current.getStartTime()) + " -> " + formatter.format(time.getStartTime()));
            }
            if (!current.getStopTime().equals(time.getStopTime())) {
                changeDelta.addItem(ChangeDeltaItemType.STOP_TIME, formatter.format(current.getStopTime()) + " -> " + formatter.format(time.getStopTime()));
            }
            em.merge(time);
            // have to recalculate the cached interval
            calculateCachedDates(activity);
            updateRemaining(key, activity, percentageComplete);
//        }
    }

    public void deleteTime(KeyLocal key, Long timeId) {
        Time time = em.find(Time.class, timeId);
        Activity activity = time.getActivity();
        key.assertHasPermission(activity, Permission.TIME_DELETE);
        ChangeDelta changeDelta = activity.addChangeDelta(ChangeDeltaType.DELETE_TIME, key.getUser());
        changeDelta.addItem(ChangeDeltaItemType.PERSON, time.getPerson().toString());
        changeDelta.addItem(ChangeDeltaItemType.DESCRIPTION, time.getDescription());
        changeDelta.addItem(ChangeDeltaItemType.STATE, time.getState().toString());
        changeDelta.addItem(ChangeDeltaItemType.START_TIME, formatter.format(time.getStartTime()));
        changeDelta.addItem(ChangeDeltaItemType.STOP_TIME, formatter.format(time.getStopTime()));
        em.persist(changeDelta);
        em.remove(time);
        // have to recalculate the cached interval
        updateRemaining(key, activity, (byte)0);
    }

    private byte calculateRemaining(KeyLocal key,
            Activity activity,
            byte percentageComplete,
            Time time) {
        int percentage = 0;
        int remaining_min = time.getRemainingDuration_min();
        int totalTime_min = activity.getTotalTime_min();
        // check for remaining time update
        if (time.getRemainingDuration_min() != activity.getRemainingDuration_min()) {
            percentage = calculatePercentage(totalTime_min, remaining_min);
        } // else check for percentage complete update
        else {
            if (percentageComplete != activity.getPercentageComplete()) {
                percentage = percentageComplete;
                if (percentage != 0) {
                    remaining_min = (100 * totalTime_min) / percentage - totalTime_min;
                } else {
                    remaining_min = activity.getExpectedDuration_min();
                }
            } // else calculate remainder and percentage given period
            else {
                long diff = time.getStopTime().getTime() - time.getStartTime().getTime();
                int diff_min = (int) (diff / (1000 * 60));
                remaining_min -= diff_min;
                if (remaining_min < 0) {
                    remaining_min = 0;
                }
                if (totalTime_min == 0) {
                    totalTime_min = activity.getExpectedDuration_min() - remaining_min;
                    if (totalTime_min < 0) {
                        totalTime_min = 0;
                    }
                }
                percentage = calculatePercentage(totalTime_min, remaining_min);
            }
        }
        // round off to nearest 5
        percentage = ((percentage + 3) / 5) * 5;
        time.setRemainingDuration_min(new Integer(remaining_min));
        return (byte)percentage;
    }

    /**
     * Find the most recent time entry and then update remaining time for the task accordingly.
     * @param key
     * @param activity
     * @param percentageComplete
     */
    private void updateRemaining(KeyLocal key, Activity activity, byte percentageComplete) {
        Time time = null;
        for (Time tl: activity.getTimes()) {
            if (time == null || tl.getStopTime().after(time.getStopTime())) {
                time = tl;
            }
        }
        // is this the most recent time entry for the task?
        if (time != null) {
            ChangeDelta changeDelta = activity.addChangeDelta(ChangeDeltaType.EDIT_TIME, key.getUser());
            if (percentageComplete != activity.getPercentageComplete()) {
                activity.setPercentageComplete(percentageComplete);
                changeDelta.addItem(ChangeDeltaItemType.PERCENTAGE_COMPLETE, Byte.toString(percentageComplete));
            }
            if (time.getRemainingDuration_min() != activity.getRemainingDuration_min()) {
                activity.setRemainingDuration_min(time.getRemainingDuration_min());
                changeDelta.addItem(ChangeDeltaItemType.EXPECTED_DURATION, activity.remainingDurationToString());
            }
            em.persist(changeDelta);
            em.merge(activity);
        }
    }

    private int calculatePercentage(int totalTime_min, int remaining_min) {
        int percentage;
        if (totalTime_min + remaining_min != 0) {
            percentage = (totalTime_min * 100) / (totalTime_min + remaining_min);
            if (percentage < 0)
                percentage = 0;
            if (percentage > 100)
                percentage = 100;
        }
        else
            percentage = 100;
	return percentage;
    }

    public Time findTime(KeyLocal key, Long timeId) {
        Time time = em.find(Time.class, timeId);
        key.assertHasPermission(time.getActivity(), Permission.NODE_VIEW);
        return time;
    }
}
